/*
 * Course:    CS 590 Web Services
 * Semseter:  Spring 2009
 * Professor: Sedlemeyer
 * Project:   CHEAT (Code Help Educational Assignment Tool)
 * Due Date:  May 1, 2009
 * Team:      Sarcasm
 *            (Baker, Jason; Mitchell, Robin; Shie, Keith)
 */
package cheat.authentication;

import cheat.entities.AccessType;
import cheat.entities.Person;
import cheat.entities.RegisteredUser;
import cheat.entities.StatusType;
import cheat.entities.UserSession;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceUnit;
import javax.persistence.Query;

/**
 * Provides the functionality related to security/user management.
 *
 * @author Keith Shie
 */
@Stateless
public class SecurityManagerBean implements SecurityManagerRemote, SecurityManagerLocal {

  @Resource(name = "jms/mail")
  private Queue mail;
  @Resource(name = "jms/mailFactory")
  private ConnectionFactory mailFactory;
  @PersistenceUnit(unitName = "CheatApp-ejbPU")
  private EntityManagerFactory emf;
  @EJB
  private SecurityManagerLocal securityManager;

  private static Logger logger = Logger.getLogger(SecurityManagerBean.class.getPackage().getName());

  /*==========================================================
   * Convienence methods
  ==========================================================*/
  private UserSession getSession( final EntityManager em, int sessionId, boolean updateAccessTime ) throws
          InvalidSessionException {
    /* do we have a session */
    final UserSession session = em.find( UserSession.class, sessionId );
    if (session == null) {
      throw new InvalidSessionException( "No session id '" + sessionId + "' found." );
    }

    if (updateAccessTime) {
      securityManager.updateSessionLastAccess( session );
    }

    return session;
  }


  private UserSession getSession( final EntityManager em, int sessionId ) throws InvalidSessionException {
    return getSession( em, sessionId, true );
  }


  private void verifyAdminAccess( final UserSession session ) throws InsufficientAccessException {
    /* do we have the required access level */
    if (!session.getUser().getAccessCode().getAccessCode().equals( AccessType.ADMIN_CODE )) {
      throw new InsufficientAccessException( "must have admin access." );
    }
  }


  private UserSession getAdminSession( final EntityManager em, int sessionId ) throws InvalidSessionException,
                                                                                      InsufficientAccessException {
    /* do we have a session */
    final UserSession session = getSession( sessionId );

    verifyAdminAccess( session );

    return session;
  }


  /*==========================================================
   * Business Methods
  ==========================================================*/
  /**
   * Utility method to retrieve the UserSession with the specified
   * <code>sessionId</code>.
   *
   * @param sessionId Id of the UserSession to retrieve.
   * @return Matching UserSession
   * @throws cheat.authentication.InvalidSessionException
   */
  public UserSession getSession( int sessionId ) throws InvalidSessionException {
    final EntityManager em = emf.createEntityManager();

    return getSession( em, sessionId, true );
  }


  /**
   * Create a new session for the user.
   * 
   * <p>This implementation allows the same user to have multiple
   * sessions active at the same time.</p>
   * 
   * <p>Ideally, this method would not be part of a public API.  However, we
   * need to gain control over the transaction so that we can force the
   * persistance layer to generate the PK for the underlying UserSession
   * Entity.  Providing this method as part of the public API is the most
   * straight forward way to gain this control.</p>
   * 
   * @param user user for which this session is being created.
   * @return UserSession which was created.
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public UserSession createNewSession( RegisteredUser user ) {
    logger.finer( "createNewSession: start" );

    final EntityManager em = emf.createEntityManager();
    UserSession newSession = new UserSession();

    newSession.setUser( user );
    Calendar now = Calendar.getInstance();
    newSession.setCreateTimeStamp( now.getTime() );
    newSession.setLastUseTimeStamp( now.getTime() );

    em.persist( newSession );
    logger.finer( "createNewSession: end" );

    return newSession;
  }


  /**
   * Update the last access time of the specified UserSession.
   *
   * <p>Ideally, this method would not be part of a public API.  However, we
   * need to gain control over the transaction so that we can force the
   * persistance layer to update the session regardless of any subsequent actions
   * (transaction rollbacks or other errors).  Providing this method as part
   * of the public API is the most straight forward way to gain this
   * control.</p>
   *
   * @param session UserSession to update.
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void updateSessionLastAccess( UserSession session ) {
    final EntityManager em = emf.createEntityManager();

    final UserSession sess = em.merge( session );

    sess.setLastUseTimeStamp( Calendar.getInstance().getTime() );

    em.persist( sess );
  }


  /**
   * Authenticate a user to the system.
   * 
   * <p>Required credentials are email address and password.</p>
   * 
   * <p><strong>NOTE:</strong> A production system would
   * should NEVER pass the user's password around as clear text!</p>
   * 
   * @param email email of the user wishing to login.
   * @param password password of the user wishing to login.
   * @return Id of the UserSession created for the user with the
   * matching credentials.
   * @throws cheat.authentication.NoSuchRegisteredUserException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRED)
  public int login( String email, String password ) throws NoSuchRegisteredUserException {
    logger.finer( "login: start" );

    final EntityManager em = emf.createEntityManager();
    final Query qry = em.createQuery(
            "select r from RegisteredUser r where r.person.email = :email and r.statCode.statCode = :statCode" );
    qry.setParameter( "email", email );
    qry.setParameter( "statCode", StatusType.ACTIVE_CD );

    NoSuchRegisteredUserException excptn = null;
    UserSession newSession = null;

    try {
      final RegisteredUser r = (RegisteredUser) qry.getSingleResult();

      final String pswd = r.getPassword();
      final boolean valid;
      if (pswd == null) {
        if (password == null) {
          valid = true;
        } else {
          valid = false;
        }
      } else if (pswd.equals( password )) {
        valid = true;
      } else {
        valid = false;
      }

      if (!valid) {
        excptn = new NoSuchRegisteredUserException( "No matching registered user found." );
      } else {
        newSession = securityManager.createNewSession( r );
        try {
          sendJMSMessageToMail( "caymanbumster@gmail.com", "Login Report ", "User " + email + " logged in" );
        } catch (JMSException ex) {
          logger.log( Level.SEVERE, ex.getMessage(), ex );
        }
      }
    } catch (NoResultException ex) {
      excptn = new NoSuchRegisteredUserException( "No matching registered user found." );
    }

    if (excptn != null) {
      throw excptn;
    }
    logger.finer( "login: end" );

    return newSession.getSessionId();
  }


  /**
   *
   * @param sessId
   * @throws cheat.authentication.InvalidSessionException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void logout( int sessId ) throws InvalidSessionException {
    logger.finer( "logout: start" );
    final EntityManager em = emf.createEntityManager();

    final UserSession session = getSession( em, sessId, false );

    em.remove( session );

    logger.finer( "logout: end" );
  }


  /**
   * Create a new Person.
   *
   * <p>Ideally, this method would not be part of a public API.  However, we
   * need to gain control over the transaction so that we can force the
   * persistance layer to generate the PK for the underlying UserSession
   * Entity.  Providing this method as part of the public API is the most
   * straight forward way to gain this control.</p>
   *
   * @param person Person to be created.
   * @return The newly created Person.
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public Person createNewPerson( Person person ) {
    logger.finer( "createNewPerson: start" );
    final EntityManager em = emf.createEntityManager();

    em.persist( person );
    logger.finer( "createNewPerson: end" );

    return person;
  }


  /**
   * <p>Create a pending, registered user.</p>
   *
   * <p>If the referenced Person already esists, that Person's
   * values will be updated and linked to the newly created
   * RegisteredUser.</p>
   *
   * <p>If a registered user with the same email already
   * exists, an exception will be thrown.</p>
   *
   * @param user
   * @return false if the user already exists.
   * @throws UserAlreadyRegisteredException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public boolean createPendingUser( RegisteredUser user ) throws UserAlreadyRegisteredException {
    logger.finer( "createPendingUser: start" );
    final EntityManager em = emf.createEntityManager();
    if (user == null || user.getPerson() == null) {
      throw new IllegalArgumentException( "'user' must be non null and have an associated 'person'." );
    }

    Person existingPerson = null;
    boolean valid = true;

    try {
      existingPerson = (Person) em.createNamedQuery( "Person.findByEmail" ).setParameter( "email", user.getPerson().
              getEmail() ).getSingleResult();

      em.refresh( existingPerson );

      if (existingPerson.getRegisteredUser() != null) {
        throw new UserAlreadyRegisteredException( "User with email '" + existingPerson.getEmail() +
                "' has already been registered." );
      } else {
        Person np = user.getPerson();

        existingPerson.setAffiliation( np.getAffiliation() );
        existingPerson.setFirstName( np.getFirstName() );
        existingPerson.setLastName( np.getLastName() );
        existingPerson.setPhone( np.getPhone() );
        existingPerson.setUrl( np.getUrl() );

        em.persist( existingPerson );
      }
    } catch (NoResultException e) {
      // this is OK, it just means we need to also add the Person
      existingPerson = securityManager.createNewPerson( user.getPerson() );
    }

    if (valid) {
      RegisteredUser newUser = new RegisteredUser( user.getUserId() );
      newUser.setPassword( user.getPassword() );
      newUser.setAccessCode( em.find( AccessType.class, user.getAccessCode().getAccessCode() ) );
      newUser.setStatCode( em.find( StatusType.class, StatusType.PENDING_CD ) );
      newUser.setPerson( existingPerson );

      em.persist( newUser );
    }

    logger.finer( "createPendingUser: end" );

    return valid;
  }


  /**
   * Provide the access rights for the specified UserSession.
   *
   * @param sessId Id of the UserSession containing the access rights.
   * @return The requested access rights.
   * @throws cheat.authentication.InvalidSessionException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public AccessType retrieveAccessRights( int sessId ) throws InvalidSessionException {
    logger.finer( "retrieveAccessRights: start" );
    final EntityManager em = emf.createEntityManager();

    final UserSession session = getSession( em, sessId );

    AccessType userAccess = session.getUser().getAccessCode();

//    em.flush();
//    em.clear();

    if (userAccess != null) {
      // create a detached object based upon the attached object.
      AccessType result = new AccessType( userAccess.getAccessCode() );
      result.setDescription( userAccess.getDescription() );

      userAccess = result;
    }

    logger.finer( "retrieveAccessRights: end" );

    return userAccess;
  }


  /**
   * <p>Obtain the list users pending approval.</p>
   *
   * <p>Only an ADMIN user is allowed to perform this operation.</p>
   *
   * @param sessionId Id of the session upon which authorization should be
   * based.
   *
   * @return List of users pending registration.
   *
   * @throws InvalidSessionException If the sessionId isn't recognized.
   * @throws InsufficientAccessException  If the associated access level of the session isn't ADMIN.
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public List<RegisteredUser> getPendingUsers( int sessionId ) throws InvalidSessionException,
                                                                      InsufficientAccessException {
    logger.finer( "getPendingUsers: start" );
    final EntityManager em = emf.createEntityManager();

    UserSession session = getAdminSession( em, sessionId );

    final List<RegisteredUser> pendingUsers = new ArrayList<RegisteredUser>();

    StatusType status = em.find( StatusType.class, StatusType.PENDING_CD );

    if (status != null) {
      em.refresh( status );
//      em.flush();
//      em.clear();

      for (RegisteredUser u : status.getReguserCollection()) {
//        u.getAccessCode().setReguserCollection( null);
//        u.getStatCode().setReguserCollection( null);
//        u.getPersonId().setAsgnPropCollection( null);
//        u.setAsgnPropCollection(null);

//        pendingUsers.add( u );
        final RegisteredUser safeU = new RegisteredUser( u.getUserId() );
        safeU.setAccessCode( new AccessType( u.getAccessCode().getAccessCode() ) );
        safeU.getAccessCode().setDescription( u.getAccessCode().getDescription() );
        safeU.setStatCode( new StatusType( u.getStatCode().getStatCode() ) );
        safeU.getStatCode().setDescription( u.getStatCode().getDescription() );
        safeU.setPerson( new Person( u.getPerson().getPersonId() ) );
        safeU.getPerson().setAffiliation( u.getPerson().getAffiliation() );
        safeU.getPerson().setEmail( u.getPerson().getEmail() );
        safeU.getPerson().setFirstName( u.getPerson().getFirstName() );
        safeU.getPerson().setLastName( u.getPerson().getLastName() );
        safeU.getPerson().setPhone( u.getPerson().getPhone() );
        safeU.getPerson().setUrl( u.getPerson().getUrl() );

        pendingUsers.add( safeU );
      }
    }

    logger.finer( "getPendingUsers: end: cnt = " + pendingUsers.size() );

    return pendingUsers;
  }


  /**
   * <p>Activate the specified pending user, allowing access to the system.</p>
   *
   * <p>Only an ADMIN user is allowed to perform this operation.</p>
   *
   * @param sessionId Id of the session upon which authorization should be
   * based.
   *
   * @param email email adress of the user to be activated.
   *
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.authentication.InsufficientAccessException
   * @throws NoSuchRegisteredUserException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void activatePendingUser( int sessionId, String email ) throws InvalidSessionException,
                                                                        InsufficientAccessException,
                                                                        NoSuchRegisteredUserException {
    logger.finer( "activatePendingUser: start" );
    final EntityManager em = emf.createEntityManager();

    getAdminSession( em, sessionId );

    try {
      final Person existingPerson = (Person) em.createNamedQuery( "Person.findByEmail" ).setParameter( "email", email ).
              getSingleResult();
      em.refresh( existingPerson );

      logger.finer( "activatePendingUser: found " + existingPerson.getPersonId() );

      final RegisteredUser user = existingPerson.getRegisteredUser();

      logger.finer( "activatePendingUser: user = " + (user == null ? "{null}" : user.getUserId() + ": " + user.
              getStatCode().getStatCode()) );
      /* If we have a pending registered user, set their status to ACTIVE */
      if (user != null && user.getStatCode().getStatCode().equals( StatusType.PENDING_CD )) {
        user.setStatCode( em.find( StatusType.class, StatusType.ACTIVE_CD ) );
        logger.fine( "activatePendingUser: promoted " + email );
        try {
          sendJMSMessageToMail( "caymanbumster@gmail.com", "User Activation", "User " + email + " has been activated." );
        } catch (JMSException ex) {
          logger.log( Level.SEVERE, ex.getMessage(), ex );
        }
      }
    } catch (NoResultException e) {
      throw new NoSuchRegisteredUserException( "no user with email '" + email + "' found" );
    }

    logger.finer( "activatePendingUser: end" );
  }


  /**
   * <p>Deny the specified pending user access to the system.</p>
   *
   * <p>Only an ADMIN user is allowed to perform this operation.</p>
   *
   * @param sessionId Id of the session upon which authorization should be
   * based.
   *
   * @param email email adress of the user to be activated.
   *
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.authentication.InsufficientAccessException
   * @throws NoSuchRegisteredUserException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void denyPendingUser( int sessionId, String email ) throws InvalidSessionException,
                                                                    InsufficientAccessException,
                                                                    NoSuchRegisteredUserException {
    logger.finer( "denyPendingUser: start" );
    final EntityManager em = emf.createEntityManager();

    getAdminSession( em, sessionId );

    try {
      final Person existingPerson = (Person) em.createNamedQuery( "Person.findByEmail" ).setParameter( "email", email ).
              getSingleResult();
      em.refresh( existingPerson );

      logger.finer( "denyPendingUser: found " + existingPerson.getPersonId() );

      final RegisteredUser user = existingPerson.getRegisteredUser();

      if (user != null) {
        em.refresh( user.getStatCode() );
      }
      logger.finer( "denyPendingUser: user = " + (user == null ? "{null}" : user.getUserId() + ": " + user.
              getStatCode().getStatCode()) );
      /* If we have a pending registered user, remove them */
      if (user != null && user.getStatCode().getStatCode().equals( StatusType.PENDING_CD )) {
        em.remove( user );
        logger.fine( "denyPendingUser: removed " + email );
        try {
          sendJMSMessageToMail( "caymanbumster@gmail.com", "User Denied", "User " + email + " has been denied access." );
        } catch (JMSException ex) {
          logger.log( Level.SEVERE, null, ex );
        }
      }
    } catch (NoResultException e) {
      throw new NoSuchRegisteredUserException( "no user with email '" + email + "' found" );
    }

    logger.finer( "denyPendingUser: end" );
  }


  /**
   * Find a Person with the specified email address.
   *
   * <p>Only an ADMIN user is allowed to perform this operation.</p>
   *
   * @param sessionId Id of the session upon which authorization should be
   * based.
   *
   * @param email email adress of Person to be retrieved.
   *
   * @return The Person specified by the email.
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.authentication.InsufficientAccessException
   */
  public Person getPerson( int sessionId, String email ) throws InvalidSessionException,
                                                                InsufficientAccessException {
    logger.finer( "getPerson: start" );
    final EntityManager em = emf.createEntityManager();

    final UserSession session = securityManager.getSession( sessionId );

    Person existingPerson = null;

    if ( session.getUser().getPerson().getEmail().equals( email)){
      //go ahead and return the details, since that's who's logged in
      existingPerson = session.getUser().getPerson();
    } else {
      // let an admin get to work
      getAdminSession( em, sessionId );

      final Query qry = em.createNamedQuery( "Person.findByEmail" );
      qry.setParameter( "email", email );

      try {
        existingPerson = (Person) qry.getSingleResult();
      } catch (NoResultException e) {
        // this is OK, just return null
      }
    }
    logger.finer( "getPerson: end" );

    return existingPerson;
  }


  private Message createJMSMessageForjmsMail( Session session, String recipient, String title, String message ) throws
          JMSException {
    // TODO create and populate message to send
    final MapMessage mm = session.createMapMessage();
    mm.setString( "recipient", recipient );
    mm.setString( "title", title );
    mm.setString( "message", message );

    return mm;
  }


  private void sendJMSMessageToMail( String recipient, String title, String message ) throws JMSException {
    Connection connection = null;
    Session session = null;
    try {
      connection = mailFactory.createConnection();
      session = connection.createSession( false, Session.AUTO_ACKNOWLEDGE );
      final MessageProducer messageProducer = session.createProducer( mail );
      messageProducer.send( createJMSMessageForjmsMail( session, recipient, title, message ) );
    } finally {
      if (session != null) {
        try {
          session.close();
        } catch (JMSException e) {
          Logger.getLogger( this.getClass().getName() ).log( Level.WARNING, "Cannot close session", e );
        }
      }
      if (connection != null) {
        connection.close();
      }
    }
  }
}
